/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;

namespace Xceed.Silverlight.Data
{
  public class PredicateExpressionBuilder
  {
    #region Constructors

    public PredicateExpressionBuilder( FilterExpressionBase expression )
    {
      m_expression = expression;
    }

    #endregion

    public Expression ToExpression( Type itemType )
    {
      MethodInfo methodInfo = PredicateExpressionBuilder.GetTwinGenericMethod(
                                MethodBase.GetCurrentMethod(),
                                itemType );

      return ( Expression )methodInfo.Invoke( this, null );
    }

    public Expression<Func<T, bool>> ToExpression<T>()
    {
      return PredicateExpressionBuilder.CreatePredicateExpression<T>(
               m_expression,
               typeof( T ),
               false,
               null );
    }

    internal Expression<Func<T, bool>> ToExpression<T>( 
      Type dataItemUnderlyingType,
      Func<string, Type> getDataMemberType )
    {
      return PredicateExpressionBuilder.CreatePredicateExpression<T>(
               m_expression,
               dataItemUnderlyingType,
               true,
               getDataMemberType );
    }

    private static Expression GetItemMemberValue(
      ParameterExpression parameter,
      string memberName )
    {
      return parameter.GetMember( memberName );
    }

    private static Func<ParameterExpression, string, Expression> GetRawDataItemMemberValue( 
      Type underlyingType,
      Func<string, Type> getDataMemberType )
    {
      return ( ParameterExpression parameter, string memberName ) =>
        {
          // Try to use an interned string to improve string comparison performance.
          string internedMemberName = string.IsInterned( memberName );

          if( internedMemberName != null )
          {
            memberName = internedMemberName;
          }

          Expression<Func<object, string, object>> getItemValueExpression =
            ( item, propertyName ) => ( ( RawDataItem )item ).GetDataMemberValue( propertyName );
          Expression getValue = Expression.Invoke(
                                  getItemValueExpression,
                                  parameter,
                                  Expression.Constant( memberName ) );

          // Find out the member type.
          Type memberType = null;
          if( getDataMemberType != null )
          {
            try
            {
              memberType = getDataMemberType.Invoke( memberName );
            }
            catch( Exception )
            {
              memberType = null;
            }
          }

          if( memberType == null )
          {
            PredicateExpressionBuilder.TryGetMemberType( underlyingType, memberName, out memberType );
          }

          // Cast the returned value to the member type if it is known.
          if( memberType != null && memberType != getValue.Type )
          {
            getValue = Expression.Convert( getValue, memberType );
          }

          return getValue;
        };
    }

    private static bool TryGetMemberType(
      Type elementType,
      string memberName,
      out Type memberType )
    {
      memberType = null;

      if( elementType == null )
        return false;

      try
      {
        PropertyInfo propertyInfo = elementType.GetProperty( memberName );
        if( propertyInfo != null )
        {
          memberType = propertyInfo.PropertyType;
        }
        else
        {
          FieldInfo fieldInfo = elementType.GetField( memberName );
          if( fieldInfo != null )
          {
            memberType = fieldInfo.FieldType;
          }
        }
      }
      catch( Exception )
      {
        // The exception is swallowed intentionally.
        // The member wasn't found on the type.
      }

      return ( memberType != null );
    }

    private static MethodInfo GetTwinGenericMethod(
      MethodBase methodInfo,
      Type genericType )
    {
      List<Type> parametersType = new List<Type>();
      ParameterInfo[] parametersInfo = methodInfo.GetParameters();

      Debug.Assert( parametersInfo.Length > 0 );

      for( int i = 0; i < parametersInfo.Length - 1; i++ )
      {
        parametersType.Add( parametersInfo[ i ].ParameterType );
      }

      MethodInfo twin = typeof( PredicateExpressionBuilder ).GetMethod(
                          methodInfo.Name,
                          parametersType.ToArray() );

      return twin.MakeGenericMethod( genericType );
    }

    private static Expression<Func<T, bool>> CreatePredicateExpression<T>(
      FilterExpressionBase filterExpression,
      Type dataItemUnderlyingType,
      bool isRawDataItem,
      Func<string, Type> getDataMemberType )
    {
      ParameterExpression parameter = Expression.Parameter( typeof( T ), null );
      Func<ParameterExpression, string, Expression> getItemMemberValue;

      if( isRawDataItem )
      {
        getItemMemberValue = PredicateExpressionBuilder.GetRawDataItemMemberValue(
                               dataItemUnderlyingType,
                               getDataMemberType );
      }
      else
      {
        getItemMemberValue = PredicateExpressionBuilder.GetItemMemberValue;
      }

      Func<string, Type> getItemMemberType = getDataMemberType;
      if( getItemMemberType == null )
      {
        Debug.Assert( dataItemUnderlyingType != null );

        getItemMemberType =
          ( memberName ) =>
          {
            PropertyInfo propertyInfo = dataItemUnderlyingType.GetProperty( memberName );
            if( propertyInfo != null )
              return propertyInfo.PropertyType;

            FieldInfo fieldInfo = dataItemUnderlyingType.GetField( memberName );
            if( fieldInfo != null )
              return fieldInfo.FieldType;

            return typeof( object );
          };
      }

      LinqExpressionBuilder expressionBuilder = new LinqExpressionBuilder(
                                                  filterExpression,
                                                  parameter,
                                                  getItemMemberValue,
                                                  getItemMemberType );
      Expression body = expressionBuilder.GetExpression();

      // Here is the comparison expression that will return every
      // item (1 == 1).  A comparison expression is used since 
      // some data source do not accept boolean values.
      if( body == null )
        return ( T item ) => 1 == 1;

      return ( Expression<Func<T, bool>> )Expression.Lambda( body, parameter );
    }

    #region Private Fields

    private FilterExpressionBase m_expression; //null

    #endregion Private Fields
  }
}
